<?php
namespace app\api\controller\v1;

use app\api\model\v1\Sn as SnModel;
use app\api\model\v1\Car as CarModel;
use app\api\model\v1\Device as DeviceModel;
use think\Db;
use jm\helper\Map;
use jm\helper\Request;

/**
 * 我的设备
 * 访问：/api/v1.device/方法名；
 */
class Device extends Base
{
    // 添加设备时，扫码后立即进行检测该设备是否存在（从redis取值）；
    public function checkIMEI() {
        try {
            $data = $this->jmParameter(['account_id', 'sn']);
            $validateResult = $this->validate($data, 
                $this->jmSetRule(['sn|设备编号' => 'require']),
                $this->jmSetMessage()
            );
            if (true !== $validateResult) {
                return $this->jmReturnJson(4001, $validateResult);
            }

            $snModel = SnModel::field(['id', 'sn'])->where(['sn' => $data['sn']])->find();
            if (is_null($snModel)) {
                return json(['errcode'=>4002, 'message'=>'该设备不存在']);
            }
            
            // 凡是调用这个接口的，都是新增；还需要判断是否重复绑定、是否已被他人绑定；
            
            $checkBindResult = $this->_checkBind($data['account_id'], $snModel->id);
            if ($checkBindResult['errcode'] === 4001) {
                return $this->jmReturnJson(4005, '请勿重复绑定');
            } else if ($checkBindResult['errcode'] === 4002) {
                return $this->jmReturnJson(4006, '此设备已被他人绑定');
            }

            return json([
                'errcode' => 200,
                'message' => '可以绑定'
            ]);
        } catch (\Exception $e) {
            return $this->jmException($e);
        }
    }

    // 重复绑定，或被他人绑定；
    private function _checkBind($accountId, $snId) {
        try {
            $bindArray = DeviceModel::where('sn_id', $snId)->column('user_id');
            if (!empty($bindArray)) {
                if (in_array($accountId, $bindArray)) {
                    return ['errcode'=>4001, 'message'=>'请勿重复绑定'];
                }
                return ['errcode'=>4002, 'message'=>'此设备已被他人绑定'];
            }
            return ['errcode'=>200, 'message'=>'可以绑定'];
        } catch (\Exception $e) {
            return $this->jmException($e, false);
        }
    }

    // <1.0>
    // 设备绑定
    public function bind()
    {
        try {
            $data = $this->jmParameter(['account_id', 'device_name', 'sn', 'car_model', 'plate_number', 'buy_at', 'action']);

            $validateResult = $this->validate($data, $this->jmSetRule([
                'device_name|设备名称'  => 'require|length:2,55',
                'sn|设备序列号'  => 'require|length:2,55',
                'car_model|车型' => 'require|length:2,25',
                'plate_number|车牌号码' => 'require|length:2,25',
                'buy_at|购车时间' => 'require|dateFormat:Y-m-d H:i:s',
                'action|操作类型' => ['require', 'regex'=>'/^(create|update)$/'],
            ]), $this->jmSetMessage(['action.regex'=>'无效的操作类型']));
            if (true !== $validateResult) {
                return json(['errcode'=>4001,'message'=>$validateResult]);
            }

            $action = $data['action'];
            unset($data['action']);

            // sn 是必须的
            // 判断是否存在该设备（去 redis 进行验证）
            $snModel = SnModel::field(['id', 'sn', 'imei'])->where(['sn' => $data['sn']])->find();
            if (is_null($snModel)) {
                return json(['errcode'=>4010,'message'=>'该设备不存在']);
            }
            
            // 如果是更新，那么 device_id 必须；
            if ($action === 'update') {
                $id = $this->jmParameter('device_id');
                if (preg_match('/^[1-9]\d*$/', $id)) {
                    $data['id'] = 1 * $id;
                } else {
                    return json(['errcode'=>4002,'message'=>'更新设备信息时，设备ID必须']);
                }
            }

            $this->jmUnsetEmptyAndTrans2int($data, ['account_id']);
            $this->jmAccountIdAndTrans2id($data);
            
            if ($action === 'create') {
                $deviceModel = new DeviceModel();
                $carModel = new CarModel();
                $deviceModel->user_id = $data['user_id'];
                $carModel->user_id = $data['user_id'];
            } else if ($action === 'update') {
                $deviceModel = DeviceModel::get($id);
                if (is_null($deviceModel)) {
                    return json(['errcode'=>4003, 'message'=>'该设备不存在']);
                }
                $carModel = CarModel::where('device_id', $deviceModel->id)->find();
                if (is_null($deviceModel)) {
                    return json(['errcode'=>4004, 'message'=>'该车辆不存在']);
                }
                $snModel = SnModel::field(['id', 'sn', 'imei'])->where(['sn' => $data['sn']])->find();
            }

            $deviceModel->name = $data['device_name'];
            $deviceModel->sn_id = $snModel->id;
            // 由于未知原因，设备上 imei 是 15位的数字编号，但上报定位位置时，在redis 中存放的是少了前面3位和最后一位的；
            // 据说是设备问题，存储到 redis 中时使用的接口并没有进行截取；难道是bug~
            // 所以在 device 表中特地保留 imei 字段，并且存放的是和 redis 中对应的 imei；
            $deviceModel->imei = substr($snModel->imei, 3, -1);

            $carModel->car_model = $data['car_model'];
            $carModel->plate_number = $data['plate_number'];
            $carModel->buy_at = $data['buy_at'];

            // 新增
            if ($action === 'create') {
                // 写入时确保处于活动的 sn 唯一（不能重复绑定，也不能交叉绑定）；
                $checkBindResult = $this->_checkBind($data['user_id'], $snModel->id);
                if ($checkBindResult['errcode'] === 4001) {
                    return $this->jmReturnJson(4005, '请勿重复绑定');
                } else if ($checkBindResult['errcode'] === 4002) {
                    return $this->jmReturnJson(4006, '此设备已被他人绑定');
                }

                // 没有绑定信息，则进行绑定（新增）；

                $carModel->image_url = '';
            }

            // 车辆图片
            $fileFail = false;
            $file = $this->request->file('carimg');
            if (!is_null($file)) {
                $uploadResult = \jm\helper\File::save($file, $carModel->image_url);

                if ($uploadResult['errcode'] === 200) {
                    $carModel->image_url = $uploadResult['data'];
                } else {
                    $fileFail = true;
                }
            }

            // 新增时有先后顺序
            if ($action === 'create') {
                $affectedRows = $deviceModel->save();
                if ($affectedRows) {
                    $carModel->device_id = $deviceModel->id;
                    $carAffectedRows = $carModel->save();
                    if ($carAffectedRows) {
                        // 用户设备数量加1；
                        Db::name('user')->where('id', $data['user_id'])->setInc('device_count');

                        if ($fileFail) {
                            return json([
                                'errcode'=>200, 
                                'message'=>'绑定设备成功，但车辆图片绑定失败，请重新绑定', 
                                'data' => [
                                    'device_id' => $deviceModel->id,
                                    'car_id' => $carModel->id
                                ]]);
                        } else {
                            return json(['errcode'=>200, 'message'=>'绑定设备成功', 'data' => [
                                'device_id' => $deviceModel->id,
                                'car_id' => $carModel->id,
                                'url' => empty($carModel->image_url) ? '' : $this->httpDomain . $carModel->image_url
                            ]]);
                        }
                    } else {
                        return json(['errcode'=>4007, 'message'=>'绑定设备失败（车辆）']);
                    }
                } else {
                    return json(['errcode'=>4008, 'message'=>'绑定设备失败（设备）']);
                }
            }

            // 更新时可能只更新车辆信息，而不更新设备信息；
            // 此时就不能进行判断device 的更新结果；因为当内容没有变化时，执行save() 返回值是 0；
            $deviceModel->isUpdate(true)->save();
            $carModel->isUpdate(true)->save();
            return json(['errcode'=>200, 'message'=>'绑定设备成功', 'data' => [
                'device_id' => $deviceModel->id,
                'car_id' => $carModel->id,
                'url' => empty($carModel->image_url) ? '' : $this->httpDomain . $carModel->image_url
            ]]);
                
        } catch (\Exception $e) {
            return $this->jmException($e);
        }
    }

    // 解除绑定
    public function unbind() {
        try {
            $data = $this->jmParameter(['account_id', 'device_id']);
            $validateResult = $this->validate($data, $this->jmSetRule([
                'device_id|设备ID' => ['require', 'regex'=>\jm\helper\Check::$regexpArray['id']],
            ]), $this->jmSetMessage(['device_id.regex'=>'设备ID无效']));
            if (true !== $validateResult) {
                return json(['errcode'=>4001,'message'=>$validateResult]);
            }

            $this->jmTrans2int($data, ['account_id', 'device_id']);

            $affectedRows1 = DeviceModel::destroy($data['device_id']);
            $affectedRows2 = CarModel::destroy(['device_id' => $data['device_id']]);

            // 用户设备数量 -1；
            Db::name('user')->where('id', $data['account_id'])->setDec('device_count');

            if ($affectedRows1 && $affectedRows2) {
                return json(['errcode'=>200, 'message'=>'解绑设备成功']);
            }

            return json(['errcode'=>4002, 'message'=>'解绑设备失败']);

        } catch (\Exception $e) {
            return $this->jmException($e);
        }
    }
   
    // <1.0>
    // 设备列表
    public function listx()
    {
        try {
            $accountId = $this->jmParameter('account_id', 0);
            $validateResult = $this->jmValidateAccountId($accountId);

            if (true !== $validateResult) {
                return json($validateResult);
            }

            $models = DeviceModel::alias('d')->field(['d.id', 'd.name', 's.sn', 'd.imei', 'c.car_model', 'c.buy_at', 'd.is_cloud', 'd.is_record',
                'IFNULL(c.plate_number, "")' => 'plate_number',
                'CASE IFNULL(c.image_url, "") WHEN "" THEN "" ELSE CONCAT("'. $this->httpDomain .'", c.image_url) END' => 'image_url',
                '"暂无数据"' => 'last_address', 'd.created_at'])
                ->join('car c', 'c.device_id=d.id', 'left')
                ->join('sn s', 's.id=d.sn_id', 'left')
                ->where('d.user_id', $accountId)
                ->order('id DESC')
                ->select();
            
            if (\think\Config::get('ifmode.glhf_fake')) {
                foreach ($models as $key => $value) {                    
                    $models[$key]->flow_rest = '1234MB';
                }
            } else {
                $imeiArr = [];
                foreach ($models as $key => $value) {
                    array_push($imeiArr, $value['imei']);
                }

                $devices = $this->_getDevicesFromRedis($imeiArr);
                foreach ($models as $key => $value) {
                    $models[$key]['status'] = 0;
                    foreach ($devices as $key2 => $value2) {
                        if ((1 * $value['imei']) === $value2['device_id']) {
                            $models[$key]['last_address'] = $value2['device_address'];
                            $tempStatus = \jm\helper\Db::isOnline($value2);
                            $models[$key]['status'] = $tempStatus . '';
                            $models[$key]['device_latlng'] = $value2['device_latlng']['coordinates'];
                            break;
                        }
                    }
                    
                    // $flowData = $this->_flow($accountId, $value['id']);
                    // if ($flowData['errcode'] === 200) {
                    //     $models[$key]['flow_rest'] = $flowData['data'];
                    // } else {
                    //     $models[$key]['flow_rest'] = '流量查询失败，请稍后刷新页面进行查询';
                    // }
                }
            }

            // 告警类型数据
            $warningType = \think\Config::get('device.warning_type');

            // 文件类型数据
            $fileType = \jm\helper\Arrayx::keepColumn(\think\Config::get('device.file_type'), ['id', 'title']);
            
            return json(['errcode'=>200, 'message'=>'获取数据成功', 'data'=>[
                'list' => $models,
                'warning_type' => $warningType,
                'file_type' => $fileType
            ]]);
        } catch (\Exception $e) {
            return $this->jmException($e);
        }
    }

    // <1.0>
    // 云备份
    public function cloud()
    {
        try {
            $data = $this->jmParameter(['account_id', 'device_id', 'is_cloud']);

            $validateResult = $this->validate($data, $this->jmSetRule([
                'device_id|设备ID' => ['require', 'regex'=>\jm\helper\Check::$regexpArray['id']],
                'is_cloud|是否自动存储云端' => 'require|number|in:0,1'
            ]), $this->jmSetMessage(['device_id.regex'=>'设备ID无效']));
            if (true !== $validateResult) {
                return json(['errcode'=>4001, 'message'=>$validateResult]);
            }
            $this->jmTrans2int($data, ['account_id', 'device_id']); 

            $condition = [
                'user_id' => $data['account_id'],
                'id' => $data['device_id']
            ];
            $this->jmUnset($data, ['account_id', 'device_id']);
            
            $affectedRows = DeviceModel::where($condition)->update($data);
            if ($affectedRows && $affectedRows > 0) {
                return json(['errcode'=>200, 'message'=>'设置成功']);
            }
            return json(['errcode'=>4002, 'message'=>'设置失败']);
        } catch (\Exception $e) {
            return $this->jmException($e);
        }
    }

    // <1.0>
    // 自动录音
    public function record()
    {
        try {
            $data = $this->jmParameter(['account_id', 'device_id', 'is_record']);

            $validateResult = $this->validate($data, $this->jmSetRule([
                'device_id|设备ID' => ['require', 'regex'=>\jm\helper\Check::$regexpArray['id']],
                'is_record|是否录音' => 'require|number|in:0,1'
            ]), $this->jmSetMessage(['device_id.regex'=>'设备ID无效']));
            if (true !== $validateResult) {
                return json(['errcode'=>4001, 'message'=>$validateResult]);
            }

            $this->jmTrans2int($data, ['account_id', 'device_id']); 

            $condition = [
                'user_id' => $data['account_id'],
                'id' => $data['device_id']
            ];
            $this->jmUnset($data, ['account_id', 'device_id']);
            
            $affectedRows = DeviceModel::where($condition)->update($data);
            if ($affectedRows && $affectedRows > 0) {
                return json(['errcode'=>200, 'message'=>'设置成功']);
            }
            return json(['errcode'=>4002, 'message'=>'设置失败']);
        } catch (\Exception $e) {
            return $this->jmException($e);
        }
    }

    // <1.0>
    // 车辆定位（redis 实时定位）
    public function gps()
    {
        try {
            $data = $this->jmParameter(['account_id', 'device_id']);

            $validateResult = $this->validate($data, 
                $this->jmSetRule(['device_id|设备ID' => ['require', 'regex'=>\jm\helper\Check::$regexpArray['id']]]),
                $this->jmSetMessage(['device_id.regex'=>'设备ID无效'])
            );
            if (true !== $validateResult) {
                return json(['errcode'=>4001,'message'=>$validateResult]);
            }

            $this->jmAccountIdAndTrans2id($data, 'device_id');
            // $data = [user_id, id]
            
            $res = $this->jmGetGPS($data['id']);
            if ($res['errcode'] === 200) {
                return json(['errcode'=>200, 'message'=>'获取实时定位数据成功', 'data'=>$res['data']]);
            }

            return json(['errcode'=>4002, 'message'=>$res['message'], 'data'=>null]);
        } catch (\Exception $e) {
            return $this->jmException($e);
        }
    }

    // <1.0>
    // 行车轨迹
    public function orbit()
    {
        try {
            // 时间范围：是按多少天计算呢，还是传递开始时间和结束时间；暂时按开始时间和结束时间；
            $data = $this->jmParameter(['account_id', 'device_id', 'start_time', 'end_time']);

            $validateResult = $this->validate($data, 
                $this->jmSetRule([
                    'device_id|设备ID' => ['require', 'regex'=>\jm\helper\Check::$regexpArray['id']],
                    'start_time|开始时间' => 'require|date',
                    'end_time|结束时间' => 'require|date'
                ]),
                $this->jmSetMessage(['device_id.regex'=>'设备ID无效'])
            );
            if (true !== $validateResult) {
                return json(['errcode'=>4001,'message'=>$validateResult]);
            }

            $this->jmAccountIdAndTrans2id($data, 'device_id');

            // 获取设备号
            $imei = $this->jmGetIMEI($data['id']);
            if (is_null($imei)) {
                return json(['errcode'=>4001, 'message'=>'该设备不存在']);
            }
           
            $retData = [];
            if (\think\Config::get('ifmode.glhf_fake')) {
                $retData = include APP_PATH . 'api/data/device-orbit.php';
            } else {
                $conn = \jm\helper\Db::getMongoIns();
                $filter = [
                    'device_id' => ['$eq' => $imei * 1],
                    'device_loctime' => ['$gte' => strtotime($data['start_time']), '$lte' => strtotime($data['end_time'])]
                ];
                $option = [
                    // 'rejection' => ['_id' => 1, 'device_id' => 1],// 不查询这些字段；
                    // 'projection' => ['_id' => 1, 'device_id' => 1],// 只查询这些字段；这两个是php扩展特有的，而不是mongodb有的东西；
                    'projection' => ['device_id' => 1, 'device_address' => 1, 'device_latlng' => 1, 'device_loctime' => 1],
                    // 'skip' => 0,
                    // 'limit' => 3
                    'sort' => ['device_loctime' => 1]
                ];
                $query = new \MongoDB\Driver\Query($filter, $option);//查询请求
                $rows = $conn->executeQuery('yunjing.locs', $query); // 执行查询

                foreach ($rows as $v) {
                    array_push($retData, [
                        'device_address' => $v->device_address,
                        'device_latlng' => $v->device_latlng->coordinates,
                        'device_loctime' => $v->device_loctime * 1000
                    ]);
                }
                
            }
            return json(['errcode'=>200, 'message'=>'获取行车轨迹数据成功', 'data'=>$retData]);
        } catch (\Exception $e) {
            return $this->jmException($e);
        }
    }

    // 从redis 中获取设备；
    // 多个设备时只能是数组参数；
    // 保证返回的都是有数据的，那些false 的不会返回；
    private function _getDevicesFromRedis($imei)
    {
        $redis = \jm\helper\Db::getRedisIns();
        
        if (is_string($imei)) {
            $imei = explode(',', $imei);
        }

        $retArr = [];
        $ret = $redis->hMGet('devices', $imei);
        if ($ret !== false) {
            foreach ($ret as $key => $device) {
                if ($device !== false) {
                    $retArr[$key] = json_decode($device, true);
                }
            }
        }

        // 多个设备时逐个取值，这个做法不可取~
        // for ($i = 0; $i < count($imei); $i++) {
        //     $device = $redis->hget('devices', $imei[$i]);
        //     if ($device !== false) {
        //         array_push($retArr, json_decode(, true));
        //     }
        // }

        return $retArr;
    }
    
    // 设备文件
    // 有 device_id，则某个具体设备的文件列表；
    // 没有 device_id，则该用户的所有设备的文件列表；
    public function fileListx()
    {
        try {
            $data = $this->jmParameter(['account_id', 'device_id', 'file_type_id', 'page']);
            $validateResult = $this->validate($data, 
                $this->jmSetRule([
                    'device_id|设备ID' => ['regex'=>\jm\helper\Check::$regexpArray['id']],
                    'file_type_id|文件类型ID' => ['regex'=>\jm\helper\Check::$regexpArray['id']],
                    // 'page|页码' => ['regex'=>\jm\helper\Check::$regexpArray['id']],
                ]),
                $this->jmSetMessage([
                    'device_id.regex' => '设备ID无效',
                    'file_type_id.regex' => '文件类型ID无效',
                    // 'page.regex' => '页码无效'
                ])
            );
            if (true !== $validateResult) {
                return $this->jmReturnJson(4001, $validateResult);
            }

            $this->jmUnsetEmptyAndTrans2int($data, ['account_id', 'device_id', 'file_type_id', 'page']);

            // if (empty($data['page'])) {
            //     $data['page'] = 1;
            // }

            // 用户名和密码
            $user = Db::name('user')->field(['username', 'password', 'password2'])->where('id', $data['account_id'])->whereNull('deleted_at')->find();
            $user['base64_username'] = base64_encode($user['username']);

            if (isset($data['device_id'])) {
                // 此时有传递，那就是只有一个设备；
                $imeiArr = DeviceModel::where('id', $data['device_id'])->column('imei');
            } else {
                // 用户所有设备的文件（暂时不显示那些解绑了的设备的文件）
                $imeiArr = DeviceModel::where('user_id', $data['account_id'])->column('imei');
            }

            if (empty($data['file_type_id'])) {
                $type = '/capture';
            } else {
                switch ($data['file_type_id']) {
                    case 1:
                        $type = '/capture';
                        break;
                    case 2:
                        $type = '';
                        break;
                    case 3:
                        $type = '/lock';
                        break;
                    default:
                        $type = '/capture';
                        break;
                }
            }

            $files = [];
            $agent = \think\Config::get('clouddisk.agent');
            foreach ($imeiArr as $key => $imei) {
                // 从云盘中获取文件
                $res = $this->_cloudDisk($imei . $type, $user['username'], substr($user['password2'], 3, -3));
                // 只有一个设备， 并且目录不存在，直接返回空数组；
                if ($res['errcode'] !== 200 && count($imeiArr) === 1) {
                    // return $this->jmReturnJson(4002, $res['message']);
                    return $this->jmReturnJson(200, '暂无数据', ['list' => []]);
                }

                // $key 是文件名，比如 "/remote.php/webdav/test/HSBCnet.mp4"；
                // $value 是文件属性的数组，比如 
                /* array(5) {
                    //["{DAV:}getcontentlength"]=> string(8) "40139260"
                    ["{DAV:}getcontenttype"]=> string(9) "video/mp4"
                    //["{DAV:}getetag"]=> string(34) ""7da2b6b38585f2d804f80434af34a935""
                    ["{DAV:}getlastmodified"]=> string(29) "Sun, 24 Mar 2019 09:58:30 GMT"
                    //["{DAV:}resourcetype"]=> NULL
                }
                其中，只有 "{DAV:}getcontenttype" 和 "{DAV:}getlastmodified" 有意义；
                而且，返回的数据是按文件名排序的， 并不是按创建时间倒序；

                额外特别说明一点，后来就是被这个坑了：
                比如，获取 .../webdav/imei 目录的文件，结果只有两种1情况：
                1，该目录下的文件，注意是直接文件，而不包含子目录下的文件，都是 .ts格式的；
                2，该目录下的子目录，包含 / 、 /capture、/lock 三个目录。
                所以，如果想要获取 capture 目录下的 .jpg 图片或者 lock 目录下的 .mp4 文件，那么上面的 $type 就必须指定为 capture 或者 lock 了。
                */
                if (isset($res['data'])) {
                    foreach ($res['data'] as $key1 => $value) {
                        // 去除目录，比如当前目录本身，以及可能的 lock 目录；
                        if (isset($value['{DAV:}getcontenttype'])) {
                            $basename = basename($key1);
                            array_push($files, [
                                'filename' => $basename,
                                // 'url' => 'http://58.57.117.132:9801' . $key,
                                'url' => $agent . $user['base64_username'] . '/' . $user['password'] . '/' . $imei . $type . '/' . $basename,
                                'thumb_url' => ''
                            ]);
                        }
                    }
                }
            }

            return $this->jmReturnJson(200, '获取文件列表成功', ['list'=>$files, 'hasMore'=>false]);
        } catch (\Exception $e) {
            return $this->jmException($e);
        }
    }
    

    // 封装云盘读取
    // 每个用户的行车记录仪上传的文件在该设备的imei为目录名称的目录下
    private function _cloudDisk($imei, $username, $password) {
        try {
            $url = \think\Config::get('clouddisk.domain') . 'remote.php/webdav/' . $imei;
            $settings = array(
                'baseUri' => $url,
                'userName' => $username,
                'password' => $password
            );
            $client = new \Sabre\DAV\Client($settings);

            $response = $client->propFind($url, array(
            // $response = $client->propFind('collection', array(
                // '{DAV:}getcontentlength',
                '{DAV:}displayname',
                '{DAV:}getcontenttype',
                // '{DAV:}getetag',
                // '{DAV:}getlastmodified'
            ), 1);
            // 当访问不存在的目录时，直接被catch了，下面这个ve 不会执行；
            // \Dev::ve($response);
            return ['errcode'=>200, 'data'=>$response];
        } catch (\Exception $e) {
            return $this->jmException($e, false);
        }
    }
    
    // 设备文件的操作（锁定、删除、解锁--暂时没有设计此功能）
    /*
    public function fileOperatex() {
        try {
            return $this->jmReturnJson(200, '操作成功');
            $data = $this->jmParameter(['account_id', 'file_id', 'file_type_id', 'action']);

            $validateResult = $this->validate($data,
                $this->jmSetRule([
                    'file_id|文件ID' => 'require',
                    'file_type_id|文件类型ID' => ['require', 'regex'=>\jm\helper\Check::$regexpArray['id']],
                    'action|操作类型' => ['require', 'regex'=>'^(lock|unlock|remove)$'] // |download
                ]),
                $this->jmSetMessage([
                    'file_type_id.regex' => '文件类型ID无效',
                    'action.regex' => '操作类型无效'
                ])
            );

            if (true !== $validateResult) {
                return $this->jmReturnJson(4001, $validateResult);
            }

            $data['file_id'] = explode(',', $data['file_id']);

            if (empty($data['file_id'])) {
                return $this->jmReturnJson(4002, '请选择文件');
            }

            $this->jmTrans2int($data, ['account_id', 'file_type_id']);

            $type = '视频';
            $fileType = \think\Config::get('device.file_type');
            foreach ($fileType as $key => $value) {
                if ($value['id'] === $data['file_type_id']) {
                    $type = $value['title'];
                    break;
                }
            }

            Db::startTrans();

            $action = '';
            switch ($data['action']) {
                case 'lock':
                    $action = '锁定';
                    $dbData = ['lock'=>'1', 'lock_at'=>\jm\helper\DateTime::YMDHISWithLine()];
                    break;
                case 'unlock':
                    $action = '解锁';
                    $dbData = ['lock'=>'0', 'unlock_at'=>\jm\helper\DateTime::YMDHISWithLine()];
                    break;
                case 'remove':
                    $action = '删除';
                    $dbData = ['deleted_at'=>\jm\helper\DateTime::YMDHISWithLine()];
                    break;
                // case 'download':
                //     if ($type === '视频') {
                //         $url = DeviceVideoModel::whereIn('id', $data['file_id'])->value('url');
                //     } else if ($type === '图片') {
                //         $url = DeviceImageModel::whereIn('id', $data['file_id'])->value('url');
                //     }
                //     // $url = '/static/upload/20190321/1a14759e0669c2b1c3fb62a53b95477f.jpg';
                //     // $url = '/static/upload/20190321/d973944256b1a913a32accbdc1b0dc6b.mp4';
                //     \jm\helper\File::download(ROOT_PATH . 'public' . $url);
                //     break;
                default:
                    break;
            }

            if ($type === '视频') {
                $affectedRows = DeviceVideoModel::whereIn('id', $data['file_id'])->update($dbData);
            } else if ($type === '图片') {
                $affectedRows = DeviceImageModel::whereIn('id', $data['file_id'])->update($dbData);
            }

            if ($affectedRows !== count($data['file_id'])) {
                Db::rollback();
                return $this->jmReturnJson(4003, $action .'失败');
            }
            Db::commit();

            return $this->jmReturnJson(200, $action .'成功');
        } catch (\Exception $e) {
            Db::rollback();
            return $this->jmException($e);
        }
    }
    */

    // <1.0>
    // 设备告警列表
    // 目前没有做上拉加载更多的功能，因为这里没有分页；
    public function warning()
    {
        try {
            $data = $this->jmParameter(['account_id', 'device_id', 'warning_type_id']);

            $validateResult = $this->validate($data, 
                $this->jmSetRule([
                    'device_id|设备ID' => ['require', 'regex'=>\jm\helper\Check::$regexpArray['id']],
                    'warning_type_id|告警类型ID' => ['regex'=>\jm\helper\Check::$regexpArray['id']]
                ]),
                $this->jmSetMessage(['device_id.regex'=>'设备ID无效','warning_type_id.regex'=>'告警类型ID无效'])
            );
            if (true !== $validateResult) {
                return json(['errcode'=>4001,'message'=>$validateResult]);
            }

            $this->jmTrans2int($data, ['account_id', 'device_id']);

            $warningTypeId = $data['warning_type_id'] ? 1 * $data['warning_type_id'] : 14;

            if (\think\Config::get('ifmode.glhf_fake')) {
                $warningArray = include APP_PATH . 'api/data/device-warning.php';

                switch ($warningTypeId) {
                    case 14:
                        $retData = $warningArray[0];
                        break;
                    case 16:
                        $retData = $warningArray[1];
                        break;
                    case 18:
                        $retData = $warningArray[2];
                        break;
                    default:
                        $retData = $warningArray[0];
                        break;
                }
            } else {
                /*
                {
                    "_id": ObjectId("5d50cd3f2e88ec2efc952b58"),
                    "alarm_device_id": NumberLong("53810053839"),
                    "alarm_device_no": "53810053839",
                    "alarm_device_name": "",
                    "alarm_device_type": 1,
                    "alarm_car_plate": "",
                    "alarm_group_id": "",
                    "alarm_group_name": "",
                    "alarm_company_id": "",
                    "alarm_company_name": "",
                    "alarm_pen_name": "",
                    "alarm_pen_id": "",
                    "alarm_latlng": {
                        "type": "Point",
                        "coordinates": [121.45777797210509, 31.205005391798736]
                    },
                    "alarm_slatlng": {
                        "type": "Point",
                        "coordinates": [121.45319666666667, 31.20690833333333]
                    },
                    "alarm_address": "上海市徐汇区天平路街道建国西路314 号合群坊,距合群坊68米",
                    "alarm_carowner": "",
                    "alarm_type": 5,
                    "alarm_loctype": 0,
                    "alarm_read": 0,
                    "alarm_user_id": "",
                    "alarm_date": 1565576509,
                    "alarm_created": 1565576510
                }
                 */
                $imei = $this->jmGetIMEI($data['device_id']);
                $conn = \jm\helper\Db::getMongoIns();
                $filter = [
                    // 'alarm_device_no' => ['$eq' => $device['imei']],
                    'alarm_device_no' => ['$eq' => $imei],
                    'alarm_type' => ['$eq' => $warningTypeId]
                ];
                $option = [
                    'projection' => [
                        '_id' => 1,
                        'alarm_device_no' => 1,
                        'alarm_car_plate' => 1,// 设计图中的报警标题没找到对应的字段，暂时显示这个吧；
                        'alarm_address' => 1,
                        'alarm_latlng' => 1,
                        'alarm_type' => 1,
                        'alarm_date' => 1// 报警时间；
                    ],
                    // 'skip' => 0,
                    'limit' => 20,
                    'sort' => ['alarm_date' => -1]
                ];
                $query = new \MongoDB\Driver\Query($filter, $option);//查询请求
                $rows = $conn->executeQuery('yunjing.alarms', $query); // 执行查询

                // 写的文档里 id 是int，但MongoDB 里是字符串；
                // Android app有问题，iPhone app 正常；
                // 为了避免app 变动，暂时通过判断来解决问题，因为 id 目前看来没什么用处；
                // 如果以后需要根据id 到 MongoDB 里查询，那时候再改 app。
                
                $retData = [];
                $i = 1;
                foreach ($rows as $v) {
                    $temp = [
                        'id' => $v->_id->__toString(),
                        'title' => $v->alarm_car_plate !== '' ? $v->alarm_car_plate : '告警标题'. mt_rand(1000, 9999),
                        'warning_at' => $v->alarm_date * 1000
                    ];
                    if ($this->isAndroid) {
                        $temp['id'] = $i++;
                    }
                    array_push($retData, $temp);
                }
            }

            return json([
                'errcode' => 200,
                'message' => '获取告警列表成功',
                'data' => $retData
            ]);
        } catch (\Exception $e) {
            return $this->jmException($e);
        }
    }

    // <1.1>
    // 流量，支持刷新查询，那就只能是一个单独接口了；
    /*
    public function flow() {
        try {
            $data = $this->jmParameter(['account_id', 'device_id']);

            $validateResult = $this->validate($data, 
                $this->jmSetRule(['device_id|设备ID' => ['require', 'regex'=>\jm\helper\Check::$regexpArray['id']]]),
                $this->jmSetMessage(['device_id.regex'=>'设备ID无效'])
            );
            if (true !== $validateResult) {
                return json(['errcode'=>4001,'message'=>$validateResult]);
            }

            $this->jmTrans2int($data, ['account_id', 'device_id']);

            $flowData = $this->_flow($data['account_id'], $data['device_id']);

            if ($flowData['errcode'] === 200) {
                return json(['errcode'=>200, 'message'=>'流量余额查询成功', 'data'=>['flow_rest'=>$flowData['data']]]);
            }
            return json(['errcode'=>4002, 'message'=>'流量余额查询失败']);
        } catch (\Exception $e) {
            return $this->jmException($e);
        }
    }
    */

    // 查询剩余流量
    /*
    private function _flow($accountId, $deviceId) {
        try {
            // ...
            return ['errcode'=>200, 'data'=>'4321MB'];
        } catch (\Exception $e) {
            return $this->jmException($e, false);
        }
    }
    */

    // <1.1>
    // 流量在线充值
    
    // 反复查询设备的当前位置和当前的电子围栏区域的关系，当超出围栏范围，调用极光推送api通知用户；
    public function checkPenAndJpush() {
        try {            
            // 根据别名推送（因为安卓手机上注册id和手机绑定的，即使更换App账户还是同一个registration_id）
            // {
            //     "audience" : {
            //         "alias" : [ "4314", "892", "4531" ]
            //     }
            // }
            // $url = 'https://api.jpush.cn/v3/push';
            // $url = 'https://bjapi.push.jiguang.cn/v3/push';// 北京机房 api 接口；
            // curl 
            //   --insecure 
            //   -X POST 
            //   -v 
            //   https://api.jpush.cn/v3/push 
            //   -H "Content-Type: application/json" 
            //   -u "7d431e42dfa6a6d693ac2d04:5e987ac6d2e04d95a9d8f0d1" 
            //   -d '{"platform":"all","audience":"all","notification":{"alert":"Hi,JPush !","android":{"extras":{"android-key1":"android-value1"}},"ios":{"sound":"sound.caf","badge":"+1","extras":{"ios-key1":"ios-value1"}}}}'
            // Authorization: Basic base64(appKey:masterSecret)
            
            // \think\Log::log('pen_jpush_000 title: 查看可推送的电子围栏');
            
            /*'SELECT p.id,p.device_id,p.longitude,p.latitude,p.radius,u.username FROM yx_pen p
            LEFT JOIN yx_device d ON d.id=p.device_id
            LEFT JOIN yx_user u ON u.id=d.user_id
            WHERE p.deleted_at IS NULL AND d.deleted_at IS NULL AND u.deleted_at IS NULL'*/
            $penArr = Db::name('pen')->alias('p')
                ->field(['p.id', 'p.device_id', 'p.longitude', 'p.latitude', 'p.radius', 'd.imei', 'p.status', 'u.username', 'u.jpush_id'])
                ->join('device d', 'd.id=p.device_id', 'left')
                ->join('user u', 'u.id=d.user_id', 'left')
                ->where('p.status', '1')// 启用了电子围栏的设备；
                ->where('p.jpush_status', '0')// 只推送一次；
                ->whereNull('p.deleted_at')
                ->whereNull('d.deleted_at')
                ->whereNull('u.deleted_at')
                ->select();
            
            // 如果没有电子围栏，什么都不用做；
            if (empty($penArr)) {
                return;
            }
            // \think\Log::log('pen_jpush_001 title: 有启用的且未推送的电子围栏');
            // \think\Log::log($penArr);

            $redis = \jm\helper\Db::getRedisIns();
            $redisAllInfo = $redis->hGetAll('devices');

            // 如果是false，或者为空（其实不存在为空的情况，当没有数据时返回的是false）
            if (empty($redisAllInfo)) {
                return;
            }

            // \think\Log::log('pen_jpush_002 title: redis中设备不为空');

            $imeiKeys = array_keys($redisAllInfo);
            $url = \think\Config::get('jpush.pushUrl');
            $appKey = \think\Config::get('jpush.appKey');
            $masterSecret = \think\Config::get('jpush.masterSecret');
            
            $request = [
                'platform' => 'all',
                'audience' => [
                    'registration_id' => []
                ],
                "notification" => [
                    "alert" => "您的设备超出了电子围栏"
                ],
                'options' => [
                    'apns_production' => true
                ]
            ];

            $alias = [];
            // $alias = ['161a3797c84df62f6e2', '1a0018970aec30a9140', '141fe1da9ee2281e0ad'];

            $distances = [];

            // 从redis 中依次查询每个设备的当前位置
            foreach ($penArr as $key => $pen) {
                if (in_array($pen['imei'], $imeiKeys)) {
                    // 二维数组的每个值都是字符串，需要解析为关联数组，然后判断是否在线，以及和当时的时间差 -- 目前按30分钟计算；
                    $deviceInfo = json_decode($redisAllInfo[$pen['imei']], true);
                    if ((1 * $deviceInfo['device_online']) === 0 || ((time() - 1 * $deviceInfo['device_loctime']) > 1800)) {
                        continue;
                    }
                    $distance = \jm\helper\Map::getDistance($pen['longitude'], $pen['latitude'], $deviceInfo['device_latlng']['coordinates'][0], $deviceInfo['device_latlng']['coordinates'][1]);
                    array_push($distances, $distance);
                    // 如果超出电子围栏半径则推送
                    if ($pen['radius'] < $distance) {
                        array_push($alias, ['pen_id'=>$pen['id'], 'registration_id'=>$pen['jpush_id']]);
                    }
                }
            }
            // \think\Log::log($distances);

            if (empty($alias)) {
                return;
            }
            // \think\Log::log('pen_jpush_003 title: 有符合推送条件的电子围栏');
            // \think\Log::log($alias);

            // // alias 和 registration_id 都是一次最多推送 1000 个设备；
            // $request['audience']['registration_id'] = $alias;
            // // \Dev::ve($request);
            // /*
            // array(4) {
            //   ["platform"]=>
            //   string(3) "all"
            //   ["audience"]=>
            //   array(1) {
            //     ["alias"]=>
            //     array(1) {
            //       [0]=>
            //       string(19) "1a0018970aec30a9140"
            //     }
            //   }
            //   ["notification"]=>
            //   array(1) {
            //     ["alert"]=>
            //     string(33) "您的设备超出了电子围栏"
            //   }
            //   ["options"]=>
            //   array(1) {
            //     ["apns_production"]=>
            //     bool(true)
            //   }
            // }
            //  */
            
            $length = count($alias);
            $loops = intval(ceil($length / 1000));
            
            for ($i = 0; $i < $loops; $i++) {
                $currAlias = [];
                $penIdArr = [];

                // 每次具体循环多少次
                // 最后一次
                if ($i === ($loops - 1)) {
                    $limit = $length - $i * 1000;
                } else {
                    $limit = ($i + 1) * 1000;
                }

                for ($j = $i * 1000; $j < $limit; $j++) {
                    array_push($currAlias, $alias[$j]['registration_id']);
                    array_push($penIdArr, $alias[$j]['pen_id']);
                }
                $request['audience']['registration_id'] = $currAlias;
                $pushResponse = Request::jpush($url, $appKey .':'. $masterSecret, json_encode($request));
                Db::name('pen')->where('id', 'IN', $penIdArr)->update(['jpush_status' => '1']);
                // \think\Log::log('pen_jpush_004  title: 推送结果'. $i .' content: '. is_string($pushResponse) ? $pushResponse : json_encode($pushResponse));
            }
        } catch (\Exception $e) {
            // \think\Log::log('pen_jpush_005 title: 推送出现异常 content: '. $e->getMessage());
            Db::name('log')->insert([
                'title' => '推送超出电子围栏信息时发生异常',
                'content' => $e->getMessage()
            ]);
        }
    }
}